package com.nervenets.general.service.impl;

import com.nervenets.general.Global;
import com.nervenets.general.entity.KeyValue;
import com.nervenets.general.enumeration.Action;
import com.nervenets.general.hibernate.TreeDomainObject;
import com.nervenets.general.hibernate.dao.BaseTreeDao;
import com.nervenets.general.service.BaseService;
import com.nervenets.general.service.TreeService;
import com.nervenets.general.utils.StringUtils;
import com.nervenets.general.web.params.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import static com.nervenets.general.utils.StringUtils.formatKey;

/**
 * 2020/6/30 12:09 created by Joe
 **/
@Slf4j
@Transactional(rollbackFor = Exception.class)
public abstract class TreeServiceImpl<
        T extends TreeDomainObject<T>,
        Dao extends BaseTreeDao<T>>
        extends CacheableServiceImpl<T, Dao> implements TreeService<T> {

    @Caching(evict = {
            @CacheEvict(value = Global.Constants.CACHE_KEY, key = "@us.formatKey(#root.targetClass.simpleName, #params.formId)"),
            @CacheEvict(value = Global.Constants.TREES_CACHE_KEY, key = "#root.targetClass.simpleName", allEntries = true)
    })
    @Override
    public <S extends BaseService<T>> T formSave(FormParams<T, S> params, HttpServletRequest request, HttpServletResponse response) {
        return super.formSave(params, request, response);
    }

    @Override
    @Caching(evict = {
            @CacheEvict(value = Global.Constants.CACHE_KEY, key = "@us.formatKey(#root.targetClass.simpleName, #entity.id)"),
            @CacheEvict(value = Global.Constants.TREES_CACHE_KEY, key = "#root.targetClass.simpleName", allEntries = true)
    })
    @Transactional
    public T save(T entity, Params... params) {
        if (entity.getParentId() > 0) {
            entity.setParentIds(findParentsById(entity.getParentId(), false).stream().map(TreeDomainObject::getId).collect(Collectors.toList()));
        }
        super.save(entity);

        Cache cache = cacheManager.getCache(Global.Constants.CACHE_KEY);
        if (null != cache) {
            findAllChildrenByParentId(entity.getId()).forEach(child -> {
                child.setParentIds(findParentsById(child.getParentId(), false).stream().map(TreeDomainObject::getId).collect(Collectors.toList()));
                child.setDeleted(entity.getDeleted());
                super.save(child);
                cache.evict(StringUtils.formatKey(getClass().getSimpleName(), child.getId()));
            });
        }

        //处理在中间插入节点的数据
        if (null != entity.getChildren() && !entity.getChildren().isEmpty()) {
            for (T child : entity.getChildren()) {
                child.setParentId(entity.getId());
                this.save(child);
            }
        }

        return entity;
    }

    @Override
    @Caching(evict = {
            @CacheEvict(value = Global.Constants.TREES_CACHE_KEY, key = "#root.targetClass.simpleName", allEntries = true)
    })
    @Transactional
    public List<T> saveAll(List<T> vars, Params... params) {
        return super.saveAll(vars, params);
    }

    @Override
    @Caching(evict = {
            @CacheEvict(value = Global.Constants.CACHE_KEY, key = "@us.formatKey(#root.targetClass.simpleName, #id)"),
            @CacheEvict(value = Global.Constants.TREES_CACHE_KEY, key = "#root.targetClass.simpleName", allEntries = true)
    })
    @Transactional
    public int updateById(long id, String field, Object value) {
        return super.updateById(id, field, value);
    }

    @Override
    @Caching(evict = {
            @CacheEvict(value = Global.Constants.CACHE_KEY, key = "@us.formatKey(#root.targetClass.simpleName, #id)"),
            @CacheEvict(value = Global.Constants.TREES_CACHE_KEY, key = "#root.targetClass.simpleName", allEntries = true)
    })
    @Transactional
    public int updateById(long id, KeyValue... keyValues) {
        return super.updateById(id, keyValues);
    }

    @Override
    @Caching(evict = {
            @CacheEvict(value = Global.Constants.CACHE_KEY, key = "@us.formatKey(#root.targetClass.simpleName, #var1.id)"),
            @CacheEvict(value = Global.Constants.TREES_CACHE_KEY, key = "#root.targetClass.simpleName", allEntries = true)
    })
    @Transactional
    public void delete(T var1) {
        beforeCheck(Action.delete, var1);
        before(Action.delete, var1);
        super.delete(var1);
        after(Action.delete, var1);
    }

    @Override
    @Caching(evict = {
            @CacheEvict(value = Global.Constants.CACHE_KEY, key = "@us.formatKey(#root.targetClass.simpleName, #var1.id)"),
            @CacheEvict(value = Global.Constants.TREES_CACHE_KEY, key = "#root.targetClass.simpleName", allEntries = true)
    })
    @Transactional
    public void deleteById(long var1) {
        T t = findOne(var1);
        delete(t);
    }

    @Override
    @Caching(evict = {
            @CacheEvict(value = Global.Constants.TREES_CACHE_KEY, key = "#root.targetClass.simpleName", allEntries = true)
    })
    @Transactional
    public void deleteAll(Iterable<? extends T> entities) {
        Cache cache = cacheManager.getCache(Global.Constants.CACHE_KEY);
        entities.forEach(t -> {
            this.delete(t);
            if (null != cache) cache.evict(formatKey(getClass().getSimpleName(), t.getId()));
        });
    }

    @Override
    public List<T> findParentsById(long id, boolean reversed) {
        List<T> list = new ArrayList<>();

        long tempId = id;

        T byId = findById(tempId).orElse(null);
        while (null != byId) {
            if (reversed) {
                list.add(0, byId);
            } else {
                list.add(byId);
            }

            tempId = byId.getParentId();
            if (0 == tempId) {
                break;
            }
            byId = findById(tempId).orElse(null);
        }

        return list;
    }

    @Override
    public List<T> findChildrenByParentId(long parentId) {
        DefaultTreeQueryParams<T> params = new DefaultTreeQueryParams<T>();
        params.setParentId(parentId);
        params.setAll(false);
        return findAllByTreeQueryParams(params);
    }

    @Override
    public long countChildrenByParentId(long parentId) {
        return count(new QueryParams<T>() {
            @Override
            public void generateSpecification(List<Predicate> predicates, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
                addEqualsPredicate(root.get("parentId"), parentId, true);
            }
        });
    }

    @Override
    public List<T> findAllChildrenByParentId(long parentId) {
        return super.findAll(new QueryParams<T>() {
            @Override
            public void generateSpecification(List<Predicate> predicates, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
                addJsonContainsPredicate(root.get("parentIds"), String.valueOf(parentId));
            }
        }, Sort.by(Sort.Direction.ASC, "queue"));
    }

    @Override
    public long countAllChildrenByParentId(long parentId) {
        return count(new QueryParams<T>() {
            @Override
            public void generateSpecification(List<Predicate> predicates, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
                addJsonContainsPredicate(root.get("parentIds"), String.valueOf(parentId));
            }
        });
    }

    private <TQP extends TreeQueryParams<T>> List<T> findAllByTreeQueryParams(TQP params) {
        return super.findAll(params, Sort.by(Sort.Direction.ASC, "queue"));
    }

    @Override
    @Cacheable(value = Global.Constants.TREES_CACHE_KEY, key = "@us.formatKey(#root.targetClass.simpleName, #params.cacheKey())", sync = true)
    public <TQP extends TreeQueryParams<T>> List<T> getTrees(TQP params) {
        List<T> results = new ArrayList<>();

        List<T> sources = findAllByTreeQueryParams(params);

        if (!sources.isEmpty()) {
            final List<T> childrenAll = findAll(new QueryParams<T>() {
                @Override
                public void generateSpecification(List<Predicate> predicates, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
                    List<Predicate> jsonContainsPredicates = new ArrayList<>();
                    for (T t : sources) {
                        jsonContainsPredicates.add(builder.isTrue(builder.function("JSON_CONTAINS", Boolean.class, root.get("parentIds"), builder.literal(String.valueOf(t.getId())))));
                    }
                    predicates.add(builder.or(jsonContainsPredicates.toArray(new Predicate[jsonContainsPredicates.size()])));
                }
            }, Sort.by(Sort.Direction.ASC, "queue"));

            sources.forEach(t -> {
                if (!t.isDeleted()) {
                    if (params.isAll() || t.isEnable()) {
                        childrenAdd(t, childrenAll, params.isAll());
                        results.add(t);
                    }
                }
            });
        }
        return results;
    }

    private void childrenAdd(T t, List<T> children, boolean all) {
        final List<T> sources = children.stream().filter(child -> t.getId().equals(child.getParentId())).collect(Collectors.toList());
        for (T child : sources) {
            if (!child.isDeleted()) {
                if (all || child.isEnable()) {
                    childrenAdd(child, children, all);
                    t.addChild(child);
                }
            }
        }
    }
}
